import sys
import glob
import types

import platform
import os
import os.path
from optparse import OptionParser
from source.machine import *
from source.builders import *
import shutil
import ext.patch


class TargetModule(object):
  """Information about the loaded build_<targetmodule>.py file. 
"""
  # - module
  # - name
  # - description
  # - versions
  # - default_version
  # - dependencies
  # - todo

  def __init__(self, targetmodule):
    self.name = targetmodule
    self.module = self._loadmodule( targetmodule )

    self.description = self.module.DESCRIPTION if 'DESCRIPTION' in dir(self.module) else None 
    self.versions = self.module.VERSIONS if 'VERSIONS' in dir(self.module) else [] 
    self.default_version = self.module.DEFAULT_VERSION if 'DEFAULT_VERSION' in dir(self.module) else None 
    self.todo = self.module.TODO if 'TODO' in dir(self.module) else None 
    self.dependencies = self.module.DEPENDS if 'DEPENDS' in dir(self.module) else None 
    self.requires = self.module.REQUIRES if "REQUIRES" in dir(self.module) else []


  def __eq__(self, other):
    return self.name == other.name 

  def print_info(self, verbose = False, verbose_newline=True):
    if verbose:
      print "Module:          %s" % str(self.module.__name__)
      print "Name:            %s" % self.name
      if len(self.versions) > 0:
        print "Version:         %s" % (" ".join(self.versions))
      if self.default_version:
        print "Default Version: %s" % self.default_version
      if self.dependencies:
        print "Dependencies:    %s" %( " ".join(self.dependencies) )
      if self.description:
        print "Description:     %s" % self.description
      if self.todo:
        print "Todo:            %s" % self.todo
      if verbose_newline:
        print ""
    else:
      print self.name

  def _loadmodule(self, target ):
    try:
      targetfile = "build_" + target
      __import__( targetfile, globals(), locals() )
      module = sys.modules[ targetfile ]
    except ImportError:
      print "ERROR: Cannot find target '%s'" % target
      exit(1)
    return module


class CheckModule(object):
  def __init__(self, checkmodule):
    self.name = checkmodule
    self.module = self._loadmodule( checkmodule )

  def _loadmodule(self, target ):
    try:
      targetfile = "check_" + target
      __import__( targetfile, globals(), locals() )
      module = sys.modules[ targetfile ]
    except ImportError:
      print "ERROR: Cannot find or load module '%s'" % target
      exit(1)
    return module



class RuntimeTargetException( Exception ):
  pass

class RuntimeTarget(object):
  """Information about the specified target: <runtimetarget>[-version][-debug|release][-shared|static]
     For example: osg-3.0-release
  """
  def __init__(self, config, target):
    self.config = config

    targetmodule, version, debug, linktype = self._splittarget( target )
    self.target = TargetModule(targetmodule)

    # Version information
    self.explicit_version = True
    if version is None:
      version = self.target.default_version
      self.explicit_version = False

    if version is not None:
      if version not in self.target.versions:
        raise RuntimeTargetException( "%s has no version: %s" % (targetmodule, version) )
    self.build_version = version

    # Debug information
    self.build_debug = debug # "debug", "release", or "debug+release"
    self.build_linktype = linktype 

    self.url = self._parseurl( self.config, self.target.module, self.build_version )
    self.builder = self._parsebuilder(self.config, self.target.module)
    self.cleartarget = None
    self.buildstatus = None
    self.debug = None
    self.dependencies = None

    #self.dependencies = {}
    #for d in deplist:
    #  self.dependencies[d.name] = d
    #
    ## Temporaries
    #self.procdeps = {}
    #for d in deplist:
    #  self.procdeps[d.name] = d.builder(self)
  
  def print_info(self, verbose = False, verbose_newline=True):
    if verbose:
      self.target.print_info(verbose, False)
      print "Build version:   %s" % self.build_version
      print "Debug:           %s" % self.build_debug
      print "Link type:       %s" % self.build_linktype
      print ""
    else:
      if self.explicit_version or self.build_debug != "debug+release":
        print "%s-%s-%s-%s" % ( self.target.name, self.build_version, self.build_debug, self.build_linktype )
      else:
        print self.target.name


  def direct_dependencies( self ):
    if self.target.dependencies:
      return [ RuntimeTarget(self.config, d) for d in self.target.dependencies ]
    return []

  def _parseurl(self, configuration, module, build_version):
    if hasattr( module, "URL"):
      if isinstance( module.URL, types.FunctionType ):
        urlstr = module.URL( configuration.machine, configuration.directories, build_version )
      else:
        urlstr = module.URL % { "VERSION" : build_version }
      return Url(urlstr)
    return None

  def _parsebuilder(self, config, module):
    # Is it a function, execute the function to get the real builder class.
    builderclass = None
    if hasattr( module, "BUILDER"):
      if isinstance( module.BUILDER, types.FunctionType ):
        builderclass = module.BUILDER( config.machine, config.directories )
      else:
        builderclass = module.BUILDER
    return builderclass


  def _splittarget(self, target ):
    tsplit = target.split("-")
    targetmodule = tsplit[0]

    # set debug variable to: 'debug', 'release', or 'debug+release'
    debug = "debug+release"
    for i in xrange( 1, len(tsplit)):
      if tsplit[i] in ["debug", "release"]:
        debug = tsplit[i] 
        tsplit.pop(i)

    # find whether to build static or shared. Default will be shared.
    linktype = "shared"
    for i in xrange( 1, len(tsplit)):
      if tsplit[i] in ["static", "shared"]:
        linktype = tsplit[i]
        tsplit.pop(i)

    version = None
    if len(tsplit) > 1:
      version = "-".join(tsplit[1:])
    return targetmodule, version, debug, linktype

  def builderinstance(self):
    if self.builder:
      return self.builder(self)
    return None

  # Builddependencies is needed for the dependency information.
  # The buildlist need to be given as the version number 
  # E.g. osg -> boost-2.3 (specific version), qt (no specific version)
  #      qt -> boost (no specific version)
  # In the context of building osg, the qt builder depends on boost-2.3
  def build(self, deplist):
    dependencymap = {}
    for d in deplist:
      if d.builder:
        dependencymap[d.target.name] = d.builderinstance()
    self.dependencies = dependencymap

    if self.build_debug == "debug+release":
      debugtargets = [True, False]
    else:
      debugtargets = [self.build_debug == "debug"]

    self.cleartarget = True
    for debug in debugtargets:
      self.debug = debug
      if self.builder:
        b = self.builderinstance()
        if not (self.debug and b.hasdebug and not b.hasdebug()):
          b.process()

      self.cleartarget = False

  def fullname(self):
    return "%s-%s-%s" % ( self.target.name, self.build_version, "debug" if self.debug else "release" )



class Build(object):
  def __init__( self, configuration, libraries ):
    self.configuration = configuration
    self.libraries = []
    self.addlibraries( libraries ) 

    if self.configuration.verbose:
      logformat = "%(levelname)8s %(message)s"
      ext.patch.logger.setLevel(ext.patch.logging.INFO)
      loghandler = ext.patch.logging.StreamHandler()
      loghandler.setFormatter(ext.patch.logging.Formatter(logformat))
      ext.patch.logger.addHandler(loghandler)


  def addlibraries(self, libraries ):
    if not isinstance(libraries, list):
      libraries = [libraries]
    self.libraries += libraries

    # Also add to system path.
    for l in libraries:
      sys.path.append( os.path.abspath( l ) )

  def targets(self):
    target = []
    for lib in self.libraries:
      for libfile in glob.iglob( os.path.join(lib, "build_*.py")):
        targetname = os.path.splitext(os.path.basename(libfile))[0]
        target.append(TargetModule( targetname[6:] ) )
    return target

  def runtimetarget(self, name):
    return RuntimeTarget(self.configuration, name)

  def dependencies(self, targets ):
    return self.buildlist( [ self.runtimetarget(t) for t in targets ])[:-1]

  def buildlist(self, targets ):
    def runtimetargetname(runtimetarget):
     return runtimetarget.target.name

    def adddep(result, d):
      if d.target.name not in map(runtimetargetname, result):
        result.append(d) 
      else:
        if d.explicit_version:
          index = map(runtimetargetname, result).index(d.target.name)
          #index = result.index(d)
          other = result[index]
          if other.explicit_version:
            # Two versions.
            result.append(d)
          else: 
            # Replace
            result[index] = d

    result = [] 
    for t in targets:
      directdeps = t.direct_dependencies()
      for d in self.buildlist(directdeps):
        adddep(result, d)
      adddep(result, t)
    return result

  def _parseRequires(self, config, module):
    # Is it a function, execute the function to get the real builder class.
    builderclass = None
    if hasattr( module, "CHECK"):
      if isinstance( module.CHECK, types.FunctionType ):
        builderclass = module.CHECK( config.machine, config.directories )
      else:
        builderclass = module.CHECK
    return builderclass




  def build_targets( self, targetnames):
    buildlist = self.buildlist( [self.runtimetarget(t) for t in targetnames] )
    statusok = True 
    # complete buildlist or only the targetnames
    if self.configuration.builddependencies:
      runtimetargets = buildlist
    else:
      runtimetargets = [self.runtimetarget(t) for t in targetnames]

##############
    # Check the requires
    checkmod = {} 
    for lib in self.libraries:
      for libfile in glob.iglob( os.path.join(lib, "check_*.py")):
        targetname = os.path.splitext(os.path.basename(libfile))[0]
        tm = CheckModule( targetname[6:] ) 
        checkmod[tm.name] = tm

    requirelist = []
    for t in runtimetargets:
      for req in t.target.requires:
        requirelist.append( req )

    for req in set(requirelist):
        mod = checkmod[req].module 
        checker = self._parseRequires(self.configuration, mod)
        c = checker()
        c.process()

    # Process the targets
    for target in runtimetargets:
      try:
        target.buildstatus = False
        target.build(buildlist)
        # Exception will not set this to True.
        target.buildstatus = True
      except NotImplementedException as e:
        print "Not implemented exception occured when building:", target.fullname(), "with the message:"
        print e
        print "Continuing..."
        statusok = False
      except Exception as e:
        print
        print "An error occured when building:", target.fullname()
        print "Working dir: ", os.getcwd()
        print 
        import traceback
        traceback.print_exc()

        if not self.configuration.continueonerrors:
          return False
        else:
          print "Continuing..."
          statusok = False

    if self.configuration.showsummary:
      self.show_summary(runtimetargets)

    return statusok

  def show_summary(self, runtimetargets):
    print 
    print
    print "Build summary"
    print "-------------"
    for t in runtimetargets:
      print "%s-%s" % (t.target.name, t.build_version), " --> ", "OK" if t.buildstatus else "FAIL"

  def build_all(self):
    self.build_targets([ t.name for t in self.targets() ] )
    
  def checkurl_all(self):
    targetnames = [ t.name for t in self.targets() ]
    buildlist = self.buildlist( [self.runtimetarget(t) for t in targetnames] )
    for b in buildlist:
      print b.url.url

    #import urlparse
    #obj = urlparse.urlparse(url)
    #exists( obj.netloc, obj.path )

    #import httplib
    #def exists(site, path):
    #  conn = httplib.HTTPConnection(site)
    #  conn.request('HEAD', path)
    #  response = conn.getresponse()
    #  conn.close()
    #  return response.status == 200

  def generate_environment(self, targetnames):
    """
    print the commands to set the runtime environment of targets to the stdout

    on windows users can put these commands in a bath file
      example: 'python buildextpath\buildext.py sometarget -s > sometargetenv.bat'
    on linux users can eval the command line this to setup the environment
      example: 'eval `python buildextpath\buildext.py sometarget -s`'
    """
    targetlist = self.buildlist( [self.runtimetarget(t) for t in targetnames] )
    builders = [t.builder(t) for t in targetlist if t.builder]
    islinux = builders[-1].machine.isLinux()
    
    #add python path (for modules like python, pyside, pyside-tools, osgswig, pyzmq, ipython)
    for b in builders:
      if (b.target.target.name == 'python'):
        os.environ['PYTHONHOME'] = b.targetpath()
        os.environ['PYTHONPATH'] = os.pathsep.join([os.path.join( pb.getpythonpath(), ("", "python" + b.libversion())[islinux], "site-packages")
                                                    for pb in builders if
                                                    'getpythonpath' in dir(pb) ] +
                                                    [os.environ.get('PYTHONPATH', '')])
    
        print "%s PYTHONHOME=" % ("set", "export")[islinux] +os.environ['PYTHONHOME']
        print "%s PYTHONPATH=" % ("set", "export")[islinux] +os.environ['PYTHONPATH']

    os.environ['PATH'] = os.pathsep.join([b.getbindir() for b in builders ]+[os.environ.get('PATH', '')])

    if islinux:
      #todo might want support other shell types
      #todo allow env generation to be used by builders (like pyside needs environment for generating files)
      os.environ['LD_LIBRARY_PATH'] = os.pathsep.join([b.getlibdir() for b in builders ] + [os.environ.get('LD_LIBRARY_PATH', '')])
      print "export LD_LIBRARY_PATH="+os.environ['LD_LIBRARY_PATH']
      print "export PATH="+os.environ['PATH']
      print "### run: eval `%s`" % ' '.join(sys.argv)
    else:
      #todo add python path
      print "set PATH="+os.environ['PATH']

